MySQL 三题整理:索引、MVCC 与事务失效
这篇笔记沉淀当前这一轮练到的 3 个题点:
- MySQL 索引为什么快,什么是回表、覆盖索引、最左前缀
- 事务隔离级别和 MVCC 是怎么配合工作的
- Spring 事务为什么会失效
第三题严格说更偏 Spring 与 AOP,但和数据库事务放在一起复习时非常高频,所以先作为事务专题延伸一起保留。
关联项目:
- [[黑马点评]]
1. MySQL 索引为什么快,什么是回表、覆盖索引、最左前缀
标准答案
MySQL 索引快,核心原因是底层一般采用 B+ 树结构。B+ 树的树高比较低,在大数据量下也通常只需要少量层级就能定位到目标数据,所以能明显减少磁盘 IO;同时叶子节点是有序连接的,对范围查询和排序也比较友好。
回表是指通过二级索引查到主键值后,还要再去主键索引里查完整行数据。因为 InnoDB 的二级索引叶子节点里存放的不是完整行,而是主键值。
覆盖索引是指本次查询需要的字段,索引本身已经全部包含了,这样就可以直接从索引里返回结果,不需要再回表,所以通常更快。
最左前缀是联合索引的使用规则。比如联合索引是 (a, b, c),查询时会尽量从最左边开始连续匹配。也就是说先用 a,在 a 确定后再看 b,在 a 和 b 都确定后再看 c。
高频追问
- 二级索引为什么会回表?
- 覆盖索引为什么更快?
- 一个查询明明走了索引,为什么还是可能慢?
- 联合索引在 B+ 树里到底是怎么排的?
(a, b, c)的最左前缀到底是什么意思?where a=1 and c=1、where b=1 and c=1、where a=1 and b>2 and c=3分别怎么用索引?
关键补缺
二级索引是什么
二级索引就是非主键索引。
在 InnoDB 里:
- 主键索引(聚簇索引)叶子节点存的是整行数据
- 二级索引(辅助索引)叶子节点存的是主键值和索引列的值
所以通过二级索引查数据时,如果查询字段不在这个索引里,就必须先拿到主键,再去主键索引查整行,这就是回表。
为什么覆盖索引更快
覆盖索引快,不是因为“也用了索引”,而是因为它避免了回表。
如果一个查询走的是普通二级索引,但查出的字段不全在索引里,那么数据库还得再去主键索引做一次 B+ 树查找。命中的数据一多,这个过程会产生大量随机 IO,所以整体仍然可能很慢。
你可以记成一句话:
覆盖索引快,是因为查询字段都在索引里,省掉了回表这一步。
联合索引在 B+ 树里的组织方式
如果联合索引是 (a, b, c),那这棵 B+ 树里的键不是分别按 a、b、c 排,而是按 (a, b, c) 这个复合键整体排序。
排序规则是:
- 先按
a排 a相同再按b排a和b都相同再按c排
所以:
a在全局上有序b只有在a确定后才局部有序c只有在a和b都确定后才局部有序
这就是最左前缀原则的底层原因。
三种典型查询怎么理解
where a = 1 and c = 1可以先利用
a=1找到一段范围,但因为中间缺了b,c不能继续像等值匹配那样充分利用索引排序能力,所以通常理解为:- 先用
a - 再对
c额外过滤
- 先用
where b = 1 and c = 1一般用不好这个联合索引,因为最左边的
a没有出现。where a = 1 and b > 2 and c = 3这时:
a能用b也能用来定位范围起点- 但一旦到了
b > 2这种范围条件,后面的c通常就不能继续完整利用索引做精确定位了
本质原因是:c 只有在 a 和 b 都固定时才有序;而 b>2 已经把 b 变成了一个范围,后面的 c 就只能在扫描过程中再过滤。`
易错点
- 不要把“索引快”只答成“因为排序了”
- 不要把回表说成“查询字段没建索引就叫回表”
- 不要把联合索引理解成“where 条件个数小于等于索引列数就能用”
2. 事务隔离级别和 MVCC 是怎么配合工作的
标准答案
MySQL 常见的四种隔离级别是:
- 读未提交
- 读已提交
- 可重复读
- 可串行化
从能力上看:
- 读未提交:脏读、不可重复读、幻读都可能发生
- 读已提交:可以避免脏读,但不可重复读和幻读仍可能发生
- 可重复读:可以避免脏读和不可重复读,在 MySQL InnoDB 里对幻读也做了比较强的控制
- 可串行化:并发能力最低,但隔离最强
MVCC 主要工作在 读已提交 和 可重复读 这两个级别中,核心是用不同的 Read View 管理快照读的可见性。
在 RC 下,每次快照读都会生成新的 Read View,所以两次读取之间,如果别的事务提交了修改,你下一次再读就可能看到新值,这样虽然避免了脏读,但仍然会发生不可重复读。
在 RR 下,事务第一次快照读生成的 Read View 会被后续快照读复用,所以能保证快照读场景下的可重复读。
但要注意,MVCC 主要解决的是快照读下的可见性问题;当前读场景下要防止别的事务插入新记录导致幻读,主要还是依赖间隙锁或临键锁,而不是把幻读简单全算成 MVCC 单独解决的。
高频追问
- RC 和 RR 在 Read View 上最大的区别是什么?
- MVCC 为什么能防脏读?
- RR 为什么能防不可重复读?
- 幻读为什么不能简单都说是 MVCC 单独解决的?
- 间隙锁和排他锁是 MVCC 的机制吗?
- 快照读和当前读分别是什么?
关键补缺
MVCC 和锁机制要拆开
这是这题最容易混的点。
MVCC
MVCC 解决的是:
- 不加锁普通读时,该看哪个版本
- 不同事务在同一时刻看到哪个事务版本的数据
它的核心依赖:
undo log- 隐藏字段
Read View
所以你可以把它概括成:
MVCC 管可见性。
锁机制
锁机制解决的是:
- 别人能不能改
- 别人能不能插
- 当前读时并发怎么控制
包括:
- 排他锁
- 间隙锁
- 临键锁
所以你可以把它概括成:
锁机制管并发修改。
为什么 RC 能防脏读,但防不了不可重复读
因为 RC 每次读都会生成新的 Read View。
未提交的数据不会进入你的可见范围,所以能防脏读;但如果别的事务在两次读之间提交了修改,你下一次生成新的 Read View 时就会看到新值,因此不可重复读仍然可能发生。
为什么 RR 能防快照读下的不可重复读
因为 RR 在事务第一次快照读时生成 Read View,后续快照读复用这张视图。
这样你多次读到的是同一事务视角下的数据,所以快照读场景能保持可重复读。
为什么幻读不能简单都算 MVCC 解决
如果只是普通快照读,在 RR 下很多时候你看起来“没有幻读”,本质是因为你一直在看第一次的快照。
但如果是:
select ... for updateupdatedelete
这种当前读,你如果想锁住一个范围,不让别人插入新记录,靠的就不是 MVCC,而是 间隙锁 / 临键锁。
间隙锁到底负责什么
更稳的说法是:
间隙锁主要是为了防止别的事务在某个范围内插入新记录,从而避免当前读场景下的幻读。
不要笼统说成“防止增加或删除”,重点是:
防插入。
易错点
- 不要把 MVCC 和间隙锁混成一个东西
- 不要把“RR 防幻读”简单粗暴全归功于 MVCC
- 不要把快照读和当前读混着答
3. Spring 事务为什么会失效
标准答案
Spring 事务失效最典型的原因是事务基于 AOP 代理实现,如果调用没有经过代理对象,事务就不会生效。最常见的是同类内部 this 自调用绕过代理。
除此之外,还有几个很高频的失效场景。第一,@Transactional 标在非 public 方法上通常不生效,因为代理默认只拦截 public 方法。第二,异常被 try-catch 吞掉了,代理感知不到异常,就不会回滚。第三,抛的是受检异常,比如 IOException,而没有配置 rollbackFor,Spring 默认也不会回滚。第四,当前对象根本没有交给 Spring 容器管理,比如自己 new 出来的对象,没有代理,自然也没有事务。第五,底层数据库如果用的是不支持事务的引擎,比如 MyISAM,那 Spring 配了事务也没用。再补一个,传播行为如果配置成 NOT_SUPPORTED 这类非事务模式,当前方法也可能看起来像事务失效。
高频追问
- 为什么 this 自调用会失效?
- 除了自调用,Spring 事务还有哪些高频失效场景?
- 为什么异常被吞掉后事务不回滚?
- 为什么 checked exception 默认不回滚?
- 为什么数据库引擎也会影响 Spring 事务是否生效?
- 哪些传播行为会让当前方法不跑在事务里?
关键补缺
Spring 事务本质依赖代理
Spring 声明式事务不是魔法,它本质上是:
- Spring 为 Bean 生成代理对象
- 调用代理对象的方法时,先开启事务
- 方法成功执行后提交
- 如果代理感知到满足规则的异常,就回滚
所以只要“不经过代理”,事务就容易失效。
常见失效场景
this 自调用
同一个类内部通过 this.xxx() 调用事务方法,不经过代理对象。
非 public 方法
默认事务代理主要拦截 public 方法,非 public 方法通常不生效。
异常被吞
如果你 try-catch 之后不继续抛,代理会认为方法执行成功,于是提交事务。
受检异常默认不回滚
Spring 默认只对:
RuntimeExceptionError
回滚。
像 IOException 这类 checked exception 默认不会回滚,所以很多项目里会显式写:
@Transactional(rollbackFor = Exception.class)
对象不是 Spring 管理的 Bean
如果对象是自己 new 的,就没有代理对象,也就没有事务切面。
数据库本身不支持事务
比如 MySQL 的 MyISAM 引擎,不支持事务,Spring 再怎么配都没法真的回滚。
传播行为配置不当
例如:
NOT_SUPPORTEDNEVER
这类传播行为会让当前方法以非事务方式执行,效果就像事务没生效。
易错点
- 不要只会答
this 自调用 - 不要说“只要报错就一定回滚”
- 不要忽略底层数据库引擎
三题主线关系
这三题虽然分属不同层,但非常适合连起来复习:
索引解决数据库查询为什么能快、什么情况下会慢。事务隔离级别与 MVCC解决数据库并发读写时,数据可见性和隔离性的本质问题。Spring 事务失效解决业务代码明明写了事务,为什么到工程落地层还是可能失效。
一句话串起来:
索引考的是查询性能,MVCC 考的是并发可见性,Spring 事务失效考的是工程层事务能不能真正生效。